Identity (SwiftUI)
View Identity とは
SwiftUI においては、2 つの View が同一かどうか?が重要で、画面遷移の見え方や描画パフォーマンスに影響する。例えば、以下のようにスクリーン上に View A が乗っていたとする。
code:text
Screen
┌--------------┐
| ┌---┐ |
| | A | |
| └---┘ |
└--------------┘
これが状態の変更により、以下のように変更されたとする。
code:text
Screen
┌--------------┐
| ┌---┐ |
| | A'| |
| └---┘ |
└--------------┘
このとき、View A と View A' が同一かどうかによって、画面遷移の仕方が以下のように異なる。
View A と View A' が別物ならば、SwiftUI は View A を非表示にした後に View A' を表示する
View A と View A' が同一ならば、SwiftUI は View A を View A' の位置にスライドさせる
View 同士が同一かどうか?の判定は、View Identity によって行われる。
View Identity には、Explicit Identity と Structual Identity の 2 種類がある。全ての View はいずれかの種類の View identity をもつ。
Explicit Identity は明示的に指定された Identity のこと。例えば、ForEach で id を指定すると、それが View Identity として利用される。この id が指定されていることで、SwiftUI はリストの移動や削除時のアニメーションを正常に行うことができる。
code:swift
List {
Section {
// ここで指定している id が Explicit Identity
ForEach(dogs, id: \.id) { dog in
// 各 DogView は dog.id を View Identity としてもつ
DogView(dog)
}
}
}
あるいは、id(_:) を利用して明示的に View Identity を指定することもできる。これは、特定の View のみ区別して扱いたい場合に役立つ。 code:swift
ScrollViewReader { proxy in
ScrollView {
// ここで View Identity を指定しておく
HeaderView(dog).id(headerID)
Text(dog.description)
Button("Scroll to Head") {
// 特定の View Identity の View までスクロールできる
withAnimation { proxy.scrollTo(headerID) }
}
}
}
SwiftUI は、View が同じ場所に止まっていれば、それらを同一の View とみなす。"同じ場所" とは View の構造上同じ場所、という意味であり、具体的には SwiftUI はこの "場所" の判別のために View の型構造を参照している。
例えば、下記のような if 文による分岐のある View 構造を考える。
code:swift
var body: some View {
if dogs.isEmpty {
EmptyMessageView()
} else {
DogList(dog)
}
}
この View の型構造は、下記のようになっている。
code:swift
some View =
_ConditionalContent<
EmptyMessageView,
DogList
全ての View は暗黙的にこのような型構造をもつことになる。そして、型構造上同じ位置の View であれば、同一の View Identity が割り振られる。
例として、再び下記のように見た目が変化する View を考えてみる。
code:text
Screen
┌--------------┐
| ┌---┐ |
| | A | |
| └---┘ |
└--------------┘
code:text
Screen
┌--------------┐
| ┌---┐ |
| | A'| |
| └---┘ |
└--------------┘
この View が以下のように定義されている場合を考える (Playground 向けのコードになっている)。if 文の true/false 時の View には別々の View Identity が割り当てられるため、View はアニメーションしない。
code:swift
import PlaygroundSupport
import SwiftUI
struct MyView: View {
@State var isOn: Bool = true
var body: some View {
VStack {
Toggle("Toggle Me", isOn: $isOn.animation())
.padding()
HStack {
if isOn {
Rectangle()
.foregroundColor(.green)
.frame(width: 100, height: 100)
.padding()
Spacer()
} else {
Spacer()
Rectangle()
.foregroundColor(.red)
.frame(width: 100, height: 100)
.padding()
}
}
}
}
}
PlaygroundPage.current.setLiveView(MyView())
実際に型定義を確認してみる。Playground で下記のようにすれば簡単に確認できる。
code:swift
print("\(type(of: MyView().body))")
実際に出力された型定義は、下記のようになっていた。true の時と false の時で、異なる位置の View が表示される構造になっているのがわかる。
code:swift
VStack<
TupleView<(
ModifiedContent<Toggle<Text>, _PaddingLayout>,
HStack<
_ConditionalContent<
// true の時の View の型
TupleView<(
ModifiedContent<
ModifiedContent<
ModifiedContent<Rectangle, _EnvironmentKeyWritingModifier<Optional<Color>>>,
_FrameLayout
,
_PaddingLayout
,
Spacer
)>,
// false の時の View の型
TupleView<(
Spacer,
ModifiedContent<
ModifiedContent<
ModifiedContent<Rectangle, _EnvironmentKeyWritingModifier<Optional<Color>>>,
_FrameLayout
,
_PaddingLayout
)>
)>
一方で、以下のように単一の View の性質を書き換える形にすると、View がスライドアニメーションと共に遷移することが確認できる。
code:swift
import PlaygroundSupport
import SwiftUI
struct MyView: View {
@State var isOn: Bool = true
var body: some View {
VStack(alignment: isOn ? .leading : .trailing) {
Toggle("Toggle Me", isOn: $isOn.animation())
.padding()
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(isOn ? .green : .red)
.padding()
}
}
}
PlaygroundPage.current.setLiveView(MyView())
これも型構造を確認すると、下記のようになっていた。分岐が消えたために先ほどよりもシンプルな型構造になっており、View の構造上 Rectangle はただ 1 箇所にしか現れていないことがわかる。
code:swift
VStack<
TupleView<
(
ModifiedContent<Toggle<Text>, _PaddingLayout>,
// Toggle の値が変わっても、構造上は同一箇所の View が表示される
ModifiedContent<
ModifiedContent<
ModifiedContent<Rectangle, _FrameLayout>,
_EnvironmentKeyWritingModifier<Optional<Color>>
,
_PaddingLayout
)
参考